package it.unibo.refolding.pico;

import it.unibo.refolding.common.CommonThings;
import it.unibo.refolding.common.DoublePair;
import it.unibo.refolding.common.LinearFunction;
import it.unibo.refolding.pico.PicoDataAnalyser.ContactPointAndSlope;
import it.unibo.refolding.pico.PicoDataAnalyser.OffsetAndDeflections;
import it.unimi.dsi.fastutil.doubles.DoubleList;

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.google.common.base.Preconditions;

/**
 * Heavily redundant class representing all information that we can info from an
 * approach/retraction pico file. We should at least make some more checks upon
 * construction.
 */
public class RedundantPicoCurveInfo {

	private static final Logger logger = Logger.getLogger(RedundantPicoCurveInfo.class.getSimpleName());
	
	public final PlainPicoData plainPicoData;
	public final ContactPointAndSlope approachContactPointAndSlope;
	public final ZLsbDeflectionLsbPoint retractionContactPoint;
	public final OffsetAndDeflections offsetAndDeflections;
	/*
	 * After de-angling (according to offsetAndDeflections), it is the distance from the
	 * X axis to the "p" percentile of all retraction points.
	 */
	public final double retractionEmpiricalBaseline;
	public final PeakOrNot[] peakOrNots;
	public final double kbtOverP;

	public RedundantPicoCurveInfo(PlainPicoData plainPicoData,
			ContactPointAndSlope approachContactPointAndSlope,
			ZLsbDeflectionLsbPoint retractionContactPoint,
			OffsetAndDeflections offsetAndDeflections,
			double retractionEmpiricalBaseline,
			PeakOrNot[] peakOrNots, double kbtOverP) {
		this.plainPicoData = plainPicoData;
		this.approachContactPointAndSlope = approachContactPointAndSlope;
		this.retractionContactPoint = retractionContactPoint;
		this.offsetAndDeflections = offsetAndDeflections;
		this.retractionEmpiricalBaseline = retractionEmpiricalBaseline;
		this.kbtOverP = kbtOverP;
		this.peakOrNots = peakOrNots;
	}

	public boolean[] copyOfPeaksIndicator() {
		boolean[] peaksIndicator = new boolean[peakOrNots.length];
		for (int i = peaksIndicator.length; --i >= 0;) {
			peaksIndicator[i] = peakOrNots[i].isPeak();
		}
		return peaksIndicator;
	}

	public List<PeakOrNot> copyOfPeaks() {
		List<PeakOrNot> list = new ArrayList<>();
		for (int i = 0; i < peakOrNots.length; ++i) {
			if (peakOrNots[i].isPeak()) {
				list.add(peakOrNots[i]);
			}
		}
		return list;
	}

	public double[] copyOfForcesNewton() {
		return offsetAndDeflections.copyOfDeflectionTimes(plainPicoData
				.deflectionLsbToNewtonsFactor());
	}

	public static enum PeakForceStrategy {
		RAW {
			@Override
			double force(double raw, double wlc) {
				return raw;
			}
		}, WLC {
			@Override
			double force(double raw, double wlc) {
				return wlc;
			}
		}, MIN {
			@Override
			double force(double raw, double wlc) {
				return Math.min(raw, wlc);
			}
		};
		
		abstract double force(double raw, double wlc);
	}
    
  public RetractionSegment retractionSegment(PeakForceStrategy peakForceStrategy) {
    List<RetractionPeak> peaks = new ArrayList<>();
    for (int i = 0; i < peakOrNots.length; ++i) {
      if (peakOrNots[i].isPeak()) {
        peaks.add(new RetractionPeak(
          peakOrNots[i].nonNegativeContourLengthMetres,
          // TODO: Should get it from fitting previous WLC.
          Math.max(0, forceAtIndexAfterContactPoint(peakOrNots[i].startSlopeIndex)),
          peakForceStrategy.force(forceAtIndexAfterContactPoint(i), wlcPeakForce(i)),
          extensionAtIndexAfterContactPointM(i)));
      }
    }
    DoublePair noise = meanAndStddevAfterLastPeakOrContactPointN();
    return new RetractionSegment(plainPicoData.retraction.velocity, peaks, noise.x, noise.y);
  }
	
	public List<DoublePair> contourLengthAndNonNegativeRuptureForceOfPeaksFromContactPointOnwards(
		PeakForceStrategy peakForceStrategy) {
		RetractionSegment rs = retractionSegment(peakForceStrategy);
		return rs.contourLengthAndNonNegativeRuptureForceOfPeaksFromContactPointOnwards();
	}

	public double forceAtIndexAfterContactPoint(int i) {
		return -offsetAndDeflections.horizontalDeflectionAfterContactPoint[i]
				* plainPicoData.deflectionLsbToNewtonsFactor();
	}
	
  public DoublePair meanAndStddevAfterLastPeakOrContactPointDefLsb() {
    int lastPeakIndex = lastPeakIndex();
    assert lastPeakIndex != 0;
    final int i;
    if (lastPeakIndex == -1) {
      // i = offsetAndDeflections.offset;
      // skip 1 nm
      lastPeakIndex = 0;
    }
    double neededZ = plainPicoData.retraction.z(offsetAndDeflections.offset + lastPeakIndex) - 1e-9
      / plainPicoData.zLsbToMetresFactor;
    i = PicoDataAnalyser.offsetOfEndingContiguousRegionNotBefore(
      plainPicoData.retraction.z, neededZ);
    final DoublePair meanAndStddev;
    if (i == plainPicoData.retraction.z.length) {
      // Negative zero so that when negated it results in a nice positive zero.
      meanAndStddev = DoublePair.of(-0.0, 0);
    } else {
      Preconditions.checkState(i >= lastPeakIndex);
      int iOffset = i - offsetAndDeflections.offset;
      meanAndStddev = CommonThings.meanAndStddevIgnoringNaN(
        offsetAndDeflections.horizontalDeflectionAfterContactPoint, iOffset,
        offsetAndDeflections.horizontalDeflectionAfterContactPoint.length);
    }
    return meanAndStddev;
  }
	
  public DoublePair meanAndStddevAfterLastPeakOrContactPointN() {
    double factor = -plainPicoData.deflectionLsbToNewtonsFactor();
    Preconditions.checkState(factor < 0);
    DoublePair defLsb = meanAndStddevAfterLastPeakOrContactPointDefLsb();
    // defLsb.y is NaN when the peak is the very last point.
    return DoublePair.of(defLsb.x * factor, Double.isNaN(defLsb.y) ? 0 : -defLsb.y * factor);
  }
    
	private double extensionAtIndexAfterContactPointZLsb(int i) {
		return retractionContactPoint.zLsb - plainPicoData.retraction.z(offsetAndDeflections.offset + i);
	}

	private double extensionAtIndexAfterContactPointM(int i) {
		return extensionAtIndexAfterContactPointZLsb(i) * plainPicoData.zLsbToMetresFactor;
	}
	
	public double wlcForceAtIndexAfterContactPoint(int i, double contourLength) {
		double x = extensionAtIndexAfterContactPointM(i) - forceAtIndexAfterContactPoint(i)
			/ plainPicoData.springConstantNPerM;
		if (x >= contourLength) {
			logger.log(Level.WARNING, "xNm={0} contourLengthNm={1}", new Object[] {
				x * 1e9, contourLength * 1e9 });
			return forceAtIndexAfterContactPoint(i);
		} else {
			// Wlc.computeEndToEndDistanceForTugOfWar(
			// kbtOverP, countourLength, plainPicoData.springConstantNPerM,
			// extensionAtIndexAfterContactPointM(i));
			return Wlc.evaluate(kbtOverP, x / contourLength);
		}
	}

	public LinearFunction retractionEndLinearFunction() {
		return retractionContactPoint
		    .linearFunctionWithSlope(approachContactPointAndSlope.slopeZLsbDefLsb);
	}

	public int peakCount() {
		return PeakOrNot.countPeaks(peakOrNots);
	}

	public DoubleList peakContourLengthsFromContactPointOnwards() {
		return PeakOrNot.contourLengths(peakOrNots);
	}

	public int lastPeakIndex() {
		return PeakOrNot.lastPeakindex(peakOrNots);
	}

	public PeakOrNot lastPeakOrNull() {
		int lastPeakIndex = lastPeakIndex();
		return lastPeakIndex == -1 ? null : peakOrNots[lastPeakIndex];
	}

	public double[] wlcPeakForces() {
		double[] f = new double[peakOrNots.length];
		for (int i = 0; i < f.length; ++i) {
			if (!peakOrNots[i].hasContorLength()) {
				f[i] = Double.NaN;
			} else {
				f[i] = wlcPeakForce(i);
			}
		}
		return f;
	}

	private double wlcPeakForce(int i) {
		return wlcForceAtIndexAfterContactPoint(
			i, peakOrNots[i].nonNegativeContourLengthMetres);
	}
	
	public PeakSlopes peakSlopes() {
	    int[] startSlopeIndices = new int[offsetAndDeflections.length()];
	    for (int i = 0; i < startSlopeIndices.length; ++i) {
	        startSlopeIndices[i] = peakOrNots[i].startSlopeIndex;
	    }
	    return PeakSlopes.of(startSlopeIndices);
	}

  public static List<PeakSlopes> peakSlopes(List<RedundantPicoCurveInfo> a) {
    List<PeakSlopes> b = new ArrayList<>();
    for (RedundantPicoCurveInfo info : a) {
      b.add(info == null ? PeakSlopes.EMPTY : info.peakSlopes());
    }
    return b;
  }
}
